package router

import (
	"fmt"
	"regexp"
	"strings"
	"context"
	"net/http"
)

var methods = []string{
	http.MethodHead,
	http.MethodGet,
	http.MethodPost,
	http.MethodPut,
	http.MethodDelete,
	http.MethodOptions,
}

type route struct {
	path    	string
	method  	string
	handler 	http.Handler
	regexp  	*regexp.Regexp
}

func (r *route) init() {
	args := regexp.MustCompile(`{.*?}`).FindAllString(r.path, -1)
	if len(args) <= 0 {
		return
	}

	pattern := r.path

	for n, arg := range args {
		name := strings.TrimLeft(strings.TrimRight(arg, "}"), "{")
		if i := strings.Index(name, ":"); i > -1 {
			ex := name[i+1:]
			if ex == "int" {
				ex = "\\d+"
			} else if ex == "string" {
				ex = "\\w+"
			}

			name := name[0:i]
			if len(name) <= 0 {
				name = fmt.Sprintf("%d", n)
			}

			pattern = strings.Replace(pattern, arg, `(?P<`+name+`>`+ex+`)`, 1)
		} else {
			pattern = strings.Replace(pattern, arg, `(?P<`+name+`>.+)`, 1)
		}
	}

	exp, err := regexp.Compile(fmt.Sprintf("^%s$", pattern))
	if err != nil {
		panic(fmt.Sprintf("path `%s` regexp compile: %s", r.path, err.Error()))
	}

	r.regexp = exp
}

func (r *route) match(path string) (_ bool, params map[string]string) {
	params = make(map[string]string)
	if r.regexp != nil {
		if ms := r.regexp.FindAllStringSubmatch(path, -1); ms != nil {
			ns := r.regexp.SubexpNames()
			for i, v := range ms[0] {
				if i > 0 {
					params[ns[i]] = v
				}
			}
			return true, params
		}
	}
	return false, params
}

type Router struct {
	middlewares     []func(http.Handler) http.Handler
	regexpRoutes 	map[string][]*route
	staticRoutes	map[string]map[string]*route
}

func NewRouter() *Router {
	router := &Router{
		middlewares:  make([]func(http.Handler) http.Handler, 0),
		regexpRoutes: make(map[string][]*route),
		staticRoutes: make(map[string]map[string]*route),
	}

	for _, method := range methods {
		router.regexpRoutes[method] = make([]*route, 0)
		router.staticRoutes[method] = make(map[string]*route)
	}

	return router
}

func (r *Router) Use(middlewares ...func(http.Handler) http.Handler) {
	r.middlewares = append(r.middlewares, middlewares...)
}

func (r *Router) Head(path string, handler http.HandlerFunc) {
	r.Route(http.MethodHead, path, handler)
}

func (r *Router) Get(path string, handler http.HandlerFunc) {
	r.Route(http.MethodGet, path, handler)
	r.Head(path, handler)
}

func (r *Router) Post(path string, handler http.HandlerFunc) {
	r.Route(http.MethodPost, path, handler)
}

func (r *Router) Put(path string, handler http.HandlerFunc) {
	r.Route(http.MethodPut, path, handler)
}

func (r *Router) Delete(path string, handler http.HandlerFunc) {
	r.Route(http.MethodDelete, path, handler)
}

func (r *Router) Options(path string, handler http.HandlerFunc) {
	r.Route(http.MethodOptions, path, handler)
}

func (r *Router) Route(method string, path string, handler http.HandlerFunc) {
	path =  "/" + strings.ToLower(strings.TrimLeft(path, "/"))
	method = strings.ToUpper(method)

	if _, ok := r.staticRoutes[method]; !ok {
		panic(fmt.Sprintf("method `%s` is not found", method))
	}

	route := &route{method: method, path: path, handler: http.HandlerFunc(handler)}
	route.init()

	if route.regexp != nil {
		r.regexpRoutes[method] = append(r.regexpRoutes[method], route)
	} else {
		r.staticRoutes[method][path] = route
	}
}

func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	if route, params := r.match(req.Method, req.URL.Path); route != nil {
		for key, value := range params {
			req = req.WithContext(context.WithValue(req.Context(), key, value))
		}
		handler := route.handler
		for i := len(r.middlewares) - 1; i >= 0; i-- {
			handler = r.middlewares[i](handler)
		}
		handler.ServeHTTP(w, req)
	} else {
		http.NotFound(w, req)
	}
}

func (r *Router) match(method string, path string) (_ *route, params map[string]string) {
	method = strings.ToUpper(method)
	if route, ok := r.staticRoutes[method][path]; ok {
		return route, nil
	}

	if routes, ok := r.regexpRoutes[method]; ok {
		for _, route := range routes {
			if ok, params := route.match(path); ok {
				return route, params
			}
		}
	}

	return nil, nil
}

func (r *Router) FileServer(path string, dir string) {
	dir, err := filepath.Abs(dir)
	if err != nil {
		panic(err)
	}
	fs := http.StripPrefix(path, http.FileServer(http.Dir(dir)))
	r.Get(path + "{:.*}", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fs.ServeHTTP(w, r)
	}))
}
